/*
    Copyright (C) 2013 maik.jablonski@jease.org

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package jfix.db4o;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import jfix.db4o.engine.PersistenceEngine;
import jfix.functor.Command;
import jfix.functor.Functors;
import jfix.functor.Predicate;
import jfix.functor.Procedure;
import jfix.functor.Supplier;
import jfix.util.Reflections;

public class ObjectDatabase {

	public static int MAINTENANCE_INTERVAL = 1 * 3600 * 1000;

	private ReadWriteLock lock;
	private ObjectRepository objectRepository;
	private PersistenceEngine persistenceEngine;
	private Timer maintenanceTimer;
	private boolean maintenanceScheduled;
	private boolean transactionInProgress;
	private boolean supplierCacheRetention;
	private Map<Supplier, Object> supplierCache;
	private List<Blob> blobsToSave;
	private List<Blob> blobsToDelete;
	private long timestamp;

	public ObjectDatabase(PersistenceEngine persistenceEngine) {
		this.lock = new ReentrantReadWriteLock();
		this.objectRepository = new ObjectRepository();
		this.supplierCache = new IdentityHashMap();
		this.persistenceEngine = persistenceEngine;
	}

	public PersistenceEngine getPersistenceEngine() {
		return persistenceEngine;
	}

	public void open() {
		lock.writeLock().lock();
		try {
			maintenanceScheduled = false;
			transactionInProgress = false;
			supplierCacheRetention = false;
			populateObjectRepository();
			startMaintenanceTimer(MAINTENANCE_INTERVAL);
		} finally {
			lock.writeLock().unlock();
		}
	}

	private void populateObjectRepository() {
		String blobDirectory = getBlobDirectory();
		for (Object obj : persistenceEngine.query()) {
			if (obj instanceof Persistent) {
				objectRepository.put(obj);
				if (obj instanceof Blob) {
					((Blob) obj).initPath(blobDirectory);
				}
			}
		}
	}

	private void startMaintenanceTimer(int period) {
		maintenanceTimer = new Timer(true);
		maintenanceTimer.schedule(new TimerTask() {
			public void run() {
				lock.writeLock().lock();
				try {
					if (maintenanceScheduled) {
						gc();
						persistenceEngine.backup();
					}
				} finally {
					maintenanceScheduled = false;
					lock.writeLock().unlock();
				}
			}
			// Minimize IO by distributing backups for different databases
		}, (int) (Math.random() * period), period);
	}

	private void stopMaintenanceTimer() {
		if (maintenanceTimer != null) {
			maintenanceTimer.cancel();
			maintenanceTimer = null;
		}
	}

	public String getBlobDirectory() {
		return persistenceEngine.getBlobDirectory();
	}

	public long getTimestamp() {
		lock.readLock().lock();
		try {
			return timestamp;
		} finally {
			lock.readLock().unlock();
		}
	}

	public void close() {
		lock.writeLock().lock();
		try {
			stopMaintenanceTimer();
			if (persistenceEngine != null) {
				persistenceEngine.close();
			}
			persistenceEngine = null;
			objectRepository = null;
			supplierCache = null;
		} finally {
			lock.writeLock().unlock();
		}
	}

	public void gc() {
		lock.writeLock().lock();
		try {
			Set orphanedValues = objectRepository
					.getGarbage(Persistent.Value.class);
			for (Object orphanedValue : orphanedValues) {
				if (orphanedValue instanceof Persistent.Value) {
					deleteDeliberately((Persistent) orphanedValue);
				}
			}
		} finally {
			lock.writeLock().unlock();
		}
	}

	public <E> List<E> query(Class<E> clazz) {
		lock.readLock().lock();
		try {
			return objectRepository.get(clazz);
		} finally {
			lock.readLock().unlock();
		}
	}

	public <E> List<E> query(Class<E> clazz, Predicate<E> predicate) {
		lock.readLock().lock();
		try {
			return Functors.filter(objectRepository.get(clazz), predicate);
		} finally {
			lock.readLock().unlock();
		}
	}

	public <E> E queryUnique(Class<E> clazz, Predicate<E> predicate) {
		lock.readLock().lock();
		try {
			List<E> result = query(clazz, predicate);
			if (result == null || result.size() != 1) {
				return null;
			}
			return result.get(0);
		} finally {
			lock.readLock().unlock();
		}
	}

	public <E> boolean isUnique(E entity, Predicate<E> predicate) {
		lock.readLock().lock();
		try {
			List<E> result = query((Class<E>) entity.getClass(), predicate);
			if (result == null) {
				return true;
			}
			if (result.size() == 1 && result.get(0) != entity) {
				return false;
			}
			if (result.size() > 1) {
				return false;
			}
			return true;
		} finally {
			lock.readLock().unlock();
		}
	}

	public boolean isStored(Persistent object) {
		lock.readLock().lock();
		try {
			return objectRepository.get(object.getClass()).contains(object);
		} finally {
			lock.readLock().unlock();
		}
	}

	public List<Persistent> queryReferrers(Persistent reference) {
		lock.readLock().lock();
		try {
			return new ArrayList<Persistent>(
					objectRepository.getReferrers(reference));
		} finally {
			lock.readLock().unlock();
		}
	}

	public <E> E query(Supplier<E> supplier) {
		lock.readLock().lock();
		try {
			E result = (E) supplierCache.get(supplier);
			if (result == null) {
				result = supplier.get();
				supplierCache.put(supplier, result);
			}
			return result;
		} finally {
			lock.readLock().unlock();
		}
	}

	public void read(Command transaction) {
		lock.readLock().lock();
		try {
			transaction.run();
		} finally {
			lock.readLock().unlock();
		}
	}

	public void write(Command transaction) {
		lock.writeLock().lock();
		try {
			persistenceEngine.begin();
			transactionInProgress = true;
			transaction.run();
			if (blobsToSave != null) {
				String blobDirectory = getBlobDirectory();
				for (Blob blob : blobsToSave) {
					blob.initPath(blobDirectory);
				}
			}
			if (blobsToDelete != null) {
				for (Blob blob : blobsToDelete) {
					blob.getFile().delete();
				}
			}
			persistenceEngine.commit();
		} catch (Throwable e) {
			persistenceEngine.rollback();
			close();
			throw new RuntimeException(e.getMessage(), e);
		} finally {
			blobsToSave = null;
			blobsToDelete = null;
			maintenanceScheduled = true;
			transactionInProgress = false;
			timestamp = System.currentTimeMillis();
			if (supplierCache != null && !supplierCacheRetention) {
				supplierCache.clear();
			}
			lock.writeLock().unlock();
		}
	}

	/**
	 * Save given object to storage and clear the supplier cache after the
	 * operation.
	 */
	public void save(final Persistent persistent) {
		lock.writeLock().lock();
		try {
			if (transactionInProgress) {
				traverseAndSave(persistent);
			} else {
				write(new Command() {
					public void run() {
						save(persistent);
					}
				});
			}
		} finally {
			lock.writeLock().unlock();
		}
	}

	/**
	 * Persist given object to storage without clearing the supplier cache.
	 */
	public void persist(final Persistent persistent) {
		lock.writeLock().lock();
		try {
			supplierCacheRetention = true;
			save(persistent);
		} finally {
			supplierCacheRetention = false;
			lock.writeLock().unlock();
		}
	}

	public void delete(final Persistent persistent) {
		lock.writeLock().lock();
		try {
			int refcount = queryReferrers(persistent).size();
			if (refcount != 0) {
				// Garbage collection to remove possible dangling references by
				// values.
				gc();
				refcount = queryReferrers(persistent).size();
				if (refcount != 0) {
					throw new RuntimeException("Deletion not possible: \""
							+ String.valueOf(persistent)
							+ "\" is still referenced by " + refcount
							+ " referrers.");
				}
			}
			if (transactionInProgress) {
				traverseAndDelete(persistent);
			} else {
				write(new Command() {
					public void run() {
						delete(persistent);
					}
				});
			}
		} finally {
			lock.writeLock().unlock();
		}
	}

	public void deleteDeliberately(final Persistent persistent) {
		lock.writeLock().lock();
		try {
			if (transactionInProgress) {
				traverseAndDelete(persistent);
			} else {
				write(new Command() {
					public void run() {
						deleteDeliberately(persistent);
					}
				});
			}
		} finally {
			lock.writeLock().unlock();
		}
	}

	private void traverseAndSave(Object candidate) {
		if (candidate != null) {
			traverseAndExecute(candidate, new Procedure() {
				public void execute(Object object) {
					traverseAndSave(object);
				}
			});
			if (candidate instanceof Persistent) {
				objectRepository.put(candidate);
				persistenceEngine.save(candidate);
				if (candidate instanceof Blob) {
					if (blobsToSave == null) {
						blobsToSave = new ArrayList();
					}
					blobsToSave.add((Blob) candidate);
				}
				return;
			}
		}
	}

	private void traverseAndDelete(Object candidate) {
		if (candidate != null) {
			// Don't cascade delete on values.
			if (!(candidate instanceof Persistent.Value)) {
				traverseAndExecute(candidate, new Procedure() {
					public void execute(Object object) {
						traverseAndDelete(object);
					}
				});
			}
			if (candidate instanceof Persistent) {
				objectRepository.remove(candidate);
				persistenceEngine.delete(candidate);
				if (candidate instanceof Blob) {
					if (blobsToDelete == null) {
						blobsToDelete = new ArrayList();
					}
					blobsToDelete.add((Blob) candidate);
				}
			}
		}
	}

	private void traverseAndExecute(Object candidate, Procedure procedure) {
		try {
			for (Field field : Reflections.getFields(candidate.getClass())) {
				executeOnValues(field.get(candidate), procedure);
			}
		} catch (Exception e) {
			throw new RuntimeException(e.getMessage(), e);
		}
	}

	private void executeOnValues(Object candidate, Procedure procedure) {
		if (candidate == null) {
			return;
		}
		if (candidate instanceof Persistent.Value || candidate instanceof Blob) {
			procedure.execute(candidate);
			return;
		}
		if (candidate instanceof Persistent.Value[]
				|| candidate instanceof Blob[]) {
			for (Object arrayItem : (Object[]) candidate) {
				executeOnValues(arrayItem, procedure);
			}
			return;
		}
	}
}
